package com.nage.multiplayer;

import java.io.IOException;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Enumeration;

import android.content.Context;
import android.util.Log;

import com.nage.engine.GameEngine;
import com.nage.engine.SimulationSystem;

public class MultiplayerSystem extends Thread {

	public static final int PORT = 5050;
	
	Context context;
	SimulationSystem simulation;
	ArrayList<byte[]> outputBuffer; // List of packets which have
												// been sent by the simulation
												// system and need to be sent
												// across the network.
	ArrayList<RemoteInput> inputBuffer; // contains all the packets that
												// have been recived from remote
												// phones and have yet to be
												// picked up by the simulation
												// system.
	Socket m_Socket; // used by both threads to get a stream.
	ServerSocket m_ServerSocket; // used if this phone is acting as server.
	MultiplayerOutputThread out;
	MultiplayerInputThread in;
	boolean m_Connected;
	public static long m_ClockDifference; // the difference in the clocks on server/client.
					// Represents the number of milliseconds the server is ahead.
	boolean m_GameRunning;
	private boolean notificationSent;
	private InetAddress remoteDeviceAddress;
	public boolean serverMode;
	public byte m_Role; // server, client or default.
	public final static byte SERVER = 0x01;
	public final static byte CLIENT = 0x02;
	public final static byte DEFAULT = 0x00;
	private SyncThread m_SyncThread;
	
	/**
	 * Forces the MultiplayerSystem to initialise in client mode.
	 * @param c
	 * @param ip The IP address of the server node.
	 */
	public MultiplayerSystem(Context c, InetAddress ip) {
		this(c, false);
		remoteDeviceAddress = ip;
	}
	
	/**
	 * Initialise the Multiplayer system and determine whether it should
	 * be forced into server mode, or if it should go into server search
	 * mode where it will look at a range of IPs and try to find a server.
	 * @param c
	 * @param serverMode
	 */
	public MultiplayerSystem(Context c, boolean serverMode) {
		context = c;
		m_GameRunning = true;

		inputBuffer = new ArrayList<RemoteInput>();
		outputBuffer = new ArrayList<byte[]>();
		this.serverMode = serverMode;
		
	}
	
	public void link(GameEngine ge) {
		simulation = ge.ss;
	}

	public RemoteInput getInput() {
		synchronized (inputBuffer) {
			if (!inputBuffer.isEmpty()) {
				return inputBuffer.remove(0);
			} else {
				return null;
			}
		}
	}

	/**
	 * Opens and connects two threads (input and output) and returns whether
	 * it was succesfull or not.
	 * @return Sucess of connection
	 */
	public boolean connect() {
		try {
			if(serverMode) {
				Log.d("MultiplayerSystem", "ServerMode: opening server socket.");
				m_ServerSocket = new ServerSocket(PORT);
				m_Socket = m_ServerSocket.accept();
				setRole(SERVER);
				m_Connected = true;
			} else {
				// first try to connect as a client.
				if(remoteDeviceAddress!=null) {
					Log.d("MultiplayerSystem", "ClientMode: opening socket to specific IP.");
					findServer(remoteDeviceAddress);
				} else if(!findServer()) {
					Log.d("MultiplayerSystem", "ClientMode failed. Switch to server mode.");
					m_ServerSocket = new ServerSocket(PORT);
					m_Socket = m_ServerSocket.accept();
					setRole(SERVER);
					m_Connected = true;
				}
			}
		} catch (Exception e) {
			// an error occured whilst searching for a server, or acting as the server.
			// basically, it didn't work... so we just can't do anything at the moment.
			Log.e("MultiplayerSystem", "Error starting multiplayer system: "+e.getMessage());
			m_Connected = false;
			return false;
		}
		
		out = new MultiplayerOutputThread(this);
		in = new MultiplayerInputThread(this);
		
		if(!m_Connected) {
			return false;
		}

		if(!out.connected || !in.connected) {
			return false;
		}
		
		try {
			syncClocks(); 
		} catch (Exception e) {
			Log.e("MultiplayerSystem", "Cannot connect: "+e.getMessage());
			return false;
		}
		
		if(m_Role==SERVER) {
			m_SyncThread = new SyncThread(this);
			m_SyncThread.start();
		}
		
		out.start();
		in.start();
		simulation.start();

		return true;

	}
	
	/**
	 * Sends packets back and forth between client and server, noting the time one is sent
	 * and the time the reponse is recieved. When the difference between these two times is 
	 * less than 20, we got a good response with no lag, so we find the difference between
	 * the client time and server time (minus the difference caused by lag) and that is the
	 * clock difference. This value is used to synchronise any time stamps.
	 * @throws Exception 
	 */
	private void syncClocks() throws Exception {
		long serverTime, clientTime, roundtripTime = 0, clockDiff = 0;
		byte[] buf = new byte[512];
		StringBuffer str = new StringBuffer();
		int i = 0;
		switch(m_Role) {
		case SERVER:
			clientTime = 0;
			serverTime = System.currentTimeMillis();
			while(true) {
				long sendTime = System.currentTimeMillis();
				out.out.write(Long.toString(serverTime).getBytes());
				out.out.write(-1);
				out.out.flush();
				in.in.read(buf);
				long recieveTime = System.currentTimeMillis();
				if((roundtripTime = recieveTime - sendTime) < 20) {
					Log.d("MultiplayerSystem", "Recieved response from client.");
					str = new StringBuffer();
					i = 0;
					while(buf[i]!=-1) {
						str.append((char)buf[i++]);
					}
					clientTime = Long.parseLong(str.toString());
					clockDiff = recieveTime - clientTime - (roundtripTime / 2);
					break;
				}
			}
						
			out.out.write(Long.toString(clockDiff).getBytes());
			out.out.write(-1);
			out.out.flush();
			out.out.write(-1);
			out.out.flush();
			Log.d("MultiplayerSystem", "RoundTripTime: "+roundtripTime);
			Log.d("MultiplayerSystem", "Clock Difference: "+clockDiff);
			m_ClockDifference = clockDiff;
			break;
		case CLIENT:
			while(true) {
				if(in.in.read(buf) < 0) {
					Log.e("MultiplayerSystem", "Problem recieving message from server");
					return;
				}
				Log.d("MultiplayerSystem", "Recieved message from server.");
				i = 0;
				if(buf[i]==-1) {
					break;
				}
				str = new StringBuffer();
				while(buf[i]!=-1) {
					str.append((char)buf[i++]);
				}
				serverTime = Long.parseLong(str.toString());
				clientTime = System.currentTimeMillis();
				out.out.write(Long.toString(clientTime).getBytes());
				out.out.write(-1);
				out.out.flush();
			}
			m_ClockDifference = -clockDiff;
			break;
		default:
			throw new Exception("Cannot sync clocks - No ROLE set.");
		}
	}

	@Override
	public void run() {
		
		if(!connect()) {
			return;
		}

		while (true) {
			if (m_GameRunning) {
				if (!outputBuffer.isEmpty() && !notificationSent) {
					synchronized (out) {
						out.notifyAll();
						notificationSent = true;
					}
				} else {
					synchronized (outputBuffer) {
						try {
							notificationSent = false;
							outputBuffer.wait();
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
				}

			} else {
				try {
					synchronized (this) {
						wait();
					}
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}

	}
	
	/**
	 * Tries to connect this instance of the engine to another instance which is
	 * running as a server and waiting for a connection. First it checks that we
	 * are on Wifi (as it only works on Wifi atm), and then it will conduct a search
	 * of IP addresses in the same subnet as itself until a connection is found.
	 * @throws Exception 
	 * @throws IOException 
	 * @throws UnknownHostException 
	 */
	private boolean findServer() throws Exception {
	
// Not working atm. Need to check if wifi is on though.		
//		Log.d("MultiplayerSystem", "Trying to get connManager from context: "+context.toString());
//		ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.WIFI_SERVICE);
//		Log.d("MultiplayerSystem", "Got connManager: "+connManager.toString());
//		NetworkInfo wifi = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
//		Log.d("MultiplayerSystem", "Got wifi info: "+wifi.toString());
//
//		if (!wifi.isConnected()) {
//			throw new Exception("Multiplayer system only works when Wifi is connected.");
//		}
		
		// We are on Wifi, so lets search for a server.
		InetAddress localIP = getLocalIp();
		byte quads[] = localIP.getAddress();
		InetAddress remoteIP;
		
		// set the final byte of IP address to 0, then test each posibility up to 255.
		byte up = quads[3], down = quads[3];
		int i = 0;
		while(m_GameRunning && i < 1) {
			i++;
			if(++up < 255) {
				quads[3] = up;
				remoteIP = InetAddress.getByAddress(quads);
				try {
					findServer(remoteIP);
					break;
				} catch (IOException e) {
					Log.i("MultiplayerSystem", "IP "+remoteIP+" unavailable. Trying next...");
				}
			}
			
			if(--down > 0) {
				quads[3] = down;
				remoteIP = InetAddress.getByAddress(quads);
				try {
					findServer(remoteIP);
					break;
				} catch (IOException e) {
					Log.i("MultiplayerSystem", "IP "+remoteIP+" unavailable. Trying next...");
				}
			}
			
		}

		return m_Connected;		
		
//		socket = new Socket(IP, PORT);
//		connected = true;
//		return true;
	}
	
	private void findServer(InetAddress ip) throws UnknownHostException, IOException {
		m_Socket = new Socket(ip, PORT);
		setRole(CLIENT);
		m_Connected = true;

	}

	private void setRole(byte b) {
		Log.d("MultiplayerSystem", "Connection sorted. Setting role to "+b);
		m_Role = b;
	}


	/**
	 * Called by the input system whe a touch event is recieved. This will add the
	 * RemoteInput object to the outputBuffer and notify it so that it can be sent
	 * to remote phone. It will check the input details to decide whether it needs
	 * to send it or if it can ignore it - eg. don't send loads of inputs for a 
	 * stationary finger.
	 * 
	 * @param p
	 */
	public void send(RemoteInput p) {
		if(!p.changed || p.owner==0x00) {
			return;
		}
		synchronized(outputBuffer) {
			try {
				Log.d("MultiplayerSystem", "Sending input packet");
				outputBuffer.add(RemoteInput.flatten(p));
			} catch (Exception e) {
				e.printStackTrace();
			}
			outputBuffer.notifyAll();
		}
	}




	public static String getLocalIpAsString() {
		return getLocalIp().getHostAddress().toString();
	}
	
	public static InetAddress getLocalIp() {
		try {
			for (Enumeration<NetworkInterface> en = NetworkInterface
					.getNetworkInterfaces(); en.hasMoreElements();) {
				NetworkInterface intf = en.nextElement();
				for (Enumeration<InetAddress> enumIpAddr = intf
						.getInetAddresses(); enumIpAddr.hasMoreElements();) {
					InetAddress inetAddress = enumIpAddr.nextElement();
					if (!inetAddress.isLoopbackAddress()) {
						// return inetAddress.getAddress();
						return inetAddress;
					}
				}
			}
		} catch (SocketException ex) {
			Log.e("MultiplayerSystem", ex.toString());
		}
		return null; 
	}

	public void onPause() {
		m_GameRunning = false;
		if(m_Connected) {
			try {
				m_Socket.close();
				out.close();
				in.close();
				if(m_Role==SERVER) {
					m_SyncThread.onPause();
					m_ServerSocket.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	public void onResume() {
		m_GameRunning = true;
		synchronized (this) {
			notify();
		}
	}


	public byte getRole() {
		return m_Role;
	}


	public void performSync() {
		try {
			byte[] sync_data = simulation.flatten_gamegraph();
			synchronized(outputBuffer) {
				try {
					outputBuffer.add(sync_data);
				} catch (Exception e) {
					e.printStackTrace();
				}
			//	Log.d("MultiplayerSystem", "sync_data sent to buffer. Notifying outputBuffer");
				outputBuffer.notifyAll();
			}		} catch (IOException e) {
			Log.e("MultiplayerSystem", "Error performing sync: "+e.getMessage());
		}
	}

	public void sendToInputSystem(RemoteInput p) {
		simulation.m_inputsystem.deliverToResponders(p);
	}

}
